package com.google.code.yargon.level;

import java.util.HashMap;

import com.google.code.yargon.ui.component.Refreshable;

/**
 * Base class of all map areas, inherited by the whole level, level parts such as room, corridor and whatever
 * 
 * @author Mika Myllynen
 *
 */
public class Area implements Refreshable {
	
	// Dimensions and array storing the contents

	protected int width;
	protected int height;
	protected Block[][] area;
	
	private boolean refresh = false;
			
	/**
	 * Construct a new area size width * height
	 * 
	 * @param width
	 * @param height
	 */
	public Area(int width, int height) {
		this.width = width;
		this.height = height;
		this.area = new Block[width][height];
		clear();
	}
	
	/**
	 * Construct a new pre-defined area
	 * 
	 * @param width
	 * @param height
	 * @param preDef
	 */
	public Area(int width, int height, Block[][] preDef) {
		this.width = width;
		this.height = height;
		this.area = preDef;
	}
	
	/**
	 * Get a block at coordinates x,y
	 * @param x
	 * @param y
	 * @return
	 */
	public Block get(int x, int y) {
		Block b = null;
		try {
			b = area[x][y];
		} catch(Exception e) {
			// Ignore, let caller handle null block
		}
		return b;
	}
	
	/**
	 * Get a block at coordinates specified by Location object 
	 * @param l
	 * @return
	 */
	public Block get(Location l) {
		return get(l.getX(), l.getY());
	}
	
	/**
	 * Get a block one step north from x,y
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public Block toNorth(int x, int y) {
		return get(x, y + 1);
	}
	
	/**
	 * Get a block one step north from Location
	 * 
	 * @param l
	 * @return
	 */
	public Block toNorth(Location l) {
		return get(l.getX(), l.getY() + 1);
	}
	
	/**
	 * Get a block one step east from x,y
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public Block toEast(int x, int y) {
		return get(x + 1 , y);
	}
	
	/**
	 * Get a block one step east from Location
	 * 
	 * @param l
	 * @return
	 */
	public Block toEast(Location l) {
		return get(l.getX() + 1, l.getY());
	}
	
	/**
	 * Get a block one step south from x,y
	 * @param x
	 * @param y
	 * @return
	 */
	public Block toSouth(int x, int y) {
		return get(x, y - 1);
	}
	
	/**
	 * Get a block one step south from Location
	 * 
	 * @param l
	 * @return
	 */
	public Block toSouth(Location l) {
		return get(l.getX(), l.getY() - 1);
	}
	
	/**
	 * Get a block one step west from x,y
	 * @param x
	 * @param y
	 * @return
	 */
	public Block toWest(int x, int y) {
		return get(x - 1, y);
	}
	
	/**
	 * Get a block one step west from Location
	 * @param l
	 * @return
	 */
	public Block toWest(Location l) {
		return get(l.getX() - 1, l.getY());
	}
		
	/**
	 * Put a block at x,y. Returns true on successful put and false if block was
	 * being put outside the area 
	 * 
	 * @param x
	 * @param y
	 * @param block
	 */
	public boolean put(int x, int y, Block block) {
		boolean ret = false;
		try {
			area[x][y] = block;
			ret = true;
		} catch(Exception e) {
			// Ignore, let caller check result
		}
		return ret;
	}
	
	/**
	 * Put a block at Location
	 * 
	 * @param l
	 * @param block
	 */
	public void put(Location l, Block block) {
		put(l.getX(), l.getY(), block);
	}
	
	/**
	 * Put an (sub)area inside this area
	 * 
	 * @param x
	 * @param y
	 * @param sub
	 */
	public void put(int x, int y, Area sub) {
		for(int sy = 0; sy < sub.getHeight(); sy++) {
			for(int sx = 0; sx < sub.getWidth(); sx++) {
				Block b = sub.get(sx, sy);
				b.setLocation(new Location(x + sx, y + sy, 0));
				put(x + sx, y + sy, b);
			}
		}	
	}
	
	/**
	 * Put an (sub)area inside this area
	 * 
	 * @param l
	 * @param sub
	 */
	public void put(Location l, Area sub) {
		put(l.getX(), l.getY(), sub);
	}
	
	/**
	 * Return the area width
	 * 
	 * @return
	 */
	public int getWidth() {
		return width;
	}
	
	/**
	 * Return the area height
	 * 
	 * @return
	 */
	public int getHeight() {
		return height;
	}
		
	/**
	 * Return true if the block at x,y is a WallBlock
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public boolean isWall(int x, int y) {
		try {
			if(area[x][y] instanceof WallBlock) {
				return true;
			}
		} catch(Exception e) {
			return false;
		}
		return false;
	}
	
	/**
	 * Return true if the block at Location is a WallBlock
	 * 
	 * @param l
	 * @return
	 */
	public boolean isWall(Location l) {
		return isWall(l.getX(), l.getY());
	}
	
	/**
	 * Return true if the block at x,y is a FloorBlock
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public boolean isFloor(int x, int y) {
		try {
			if(area[x][y] instanceof FloorBlock) {
				return true;
			}
		} catch(Exception e) {
			return false;
		}
		return false;
	}
	
	/**
	 * Return true if the block at Location is a FloorBlock
	 * 
	 * @param l
	 * @return
	 */
	public boolean isFloor(Location l) {
		return isFloor(l.getX(), l.getY());
	}

	/**
	 * Return true if the block at x,y is a TunnelBlock
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public boolean isTunnel(int x, int y) {
		try {
			if(area[x][y] instanceof TunnelBlock) {
				return true;
			}
		} catch(Exception e) {
			return false;
		}
		return false;
	}
	
	/**
	 * Return true if the block at Location is a TunnelBlock
	 * 
	 * @param l
	 * @return
	 */
	public boolean isTunnel(Location l) {
		return isTunnel(l.getX(), l.getY());
	}
	
	/**
	 * Return true if the block at x,y is a DoorBlock
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public boolean isDoor(int x, int y) {
		try {
			if(area[x][y] instanceof DoorBlock) {
				return true;
			}
		} catch(Exception e) {
			return false;
		}
		return false;
	}
	
	/**
	 * Return true if the block at Location is a DoorBlock
	 * 
	 * @param l
	 * @return
	 */
	public boolean isDoor(Location l) {
		return isDoor(l.getX(), l.getY());
	}
	
	/**
	 * Return true if the block at x,y is a UndefinedBlock
	 * Opposite to other test methods, this will return true also for a location outside the Area
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public boolean isUndefined(int x, int y) {
		try {
			if(area[x][y] instanceof UndefinedBlock) {
				return true;
			}
		} catch(Exception e) {
			return true;		// Opposite to other tests, block outside the area is considered undefined
		}
		return false;
	}
	
	/**
	 * Return true if the block at Location is a UndefinedBlock
	 * Opposite to other test methods, this will return true also for a location outside the Area
	 * 
	 * @param l
	 * @return
	 */
	public boolean isUndefined(Location l) {
		return isUndefined(l.getX(), l.getY());
	}

	/**
	 * Return true if the block at x,y is free
	 * Essentially same as isUndefined, but a block outside the Area is not considered free
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	public boolean isFree(int x, int y) {
		try {
			if(area[x][y] instanceof UndefinedBlock) {
				return true;
			}
		} catch(Exception e) {
			return false;		
		}
		return false;
	}
	
	/**
	 * Return true if the block at x,y is free
	 * Essentially same as isUndefined, but a block outside the Area is not considered free
	 * 
	 * @param l
	 * @return
	 */
	public boolean isFree(Location l) {
		return isFree(l.getX(), l.getY());
	}
	
	/**
	 * Return true if the block at x,y is one of given types
	 * 
	 * @param x
	 * @param y
	 * @param c
	 * @return
	 */
	public boolean isOfType(int x, int y, Class<?> ... c) {
		for(int i = 0; i < c.length; i++) {
			Class<?> a = null;
			try {
				a = area[x][y].getClass();
			} catch(Exception e) {
				a = UndefinedBlock.class;
			}	
			if(a == c[i]) {		
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Return true if the block at Location is of given type
	 * 
	 * @param l
	 * @param c
	 * @return
	 */
	public boolean isOfType(Location l, Class<?> ... c) {
		return isOfType(l.getX(), l.getY(), c);
	}
	
	/**
	 * Return a sub area from this Area
	 * 
	 * @param x
	 * @param y
	 * @param w
	 * @param h
	 * @return
	 */
	public Area subArea(int x, int y, int w, int h) {
		Area sub = new Area(w, h);
		for(int sy = 0; sy < h; sy++) {
			for(int sx = 0; sx < w; sx++) {
				sub.put(sx, sy, get(sx + x, sy + y));
			}
		}
		return sub;
	}
	
	/**
	 * Check if given sub area is free (all blocks undefined)
	 * @param sub
	 * @return
	 */
	public boolean isFree(Area sub) {
		for(int y = 0; y < sub.getHeight(); y++) {
			for(int x = 0; x < sub.getWidth(); x++) {
				if(! sub.isUndefined(x, y)) {
					return false;
				}
			}
		}
		return true;
	}
	
	/**
	 * Compile a list of blocks of given type (class) in a hash map. The block is the key and the value is an integer initially set to zero.
	 * 
	 * @param c
	 * @return
	 */
	public HashMap<Block, Integer> list(Class<?> c) {
		HashMap<Block, Integer> list = new HashMap<Block, Integer>();
	
		for(int y = 0; y < height; y++) {
			for(int x = 0; x < width; x++) {
				if(isOfType(x, y, c)) {
					list.put(get(x, y), 0);
				}
			}	
		}
		return list;
	}
	
	/**
	 * Clear the area. Fills the area with UndefinedBlock's
	 * 
	 */
	public void clear() {
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				area[x][y] = new UndefinedBlock();
			}
		}
	}
	
	/**
	 * Return a string (kind of a map) describing the area
	 *  
	 */
	public String toString() {
		String str = "";
		for(int y = height - 1; y > -1; y--) {
			for(int x = 0; x < width; x++) {
				str += area[x][y].getChar() +" ";
			}
			str += "\n";
		}
		return str;
	}

	@Override
	public void setRefresh(boolean refresh) {
		this.refresh = refresh;
		
	}

	@Override
	public boolean needRefresh() {
		return this.refresh;
	}
}
